var aop = require("./aop").aop;

exports.clyffeCache = function(maxNumObjs, maxObjSize) {
    maxNumObjs = maxNumObjs || 1000;
    maxObjSize = maxObjSize || 10*1000*1000; //~10MB
    var itself = { },
        cache = { },
        cacheLength = 0,
        cacheHits = 0,
        cacheMisses = 0,
        cacheNearMisses = 0,
        findCacheId = function(request) {
            var type = "*/*",
                uri = request.url,
                allowedTypes = (request.headers["accept"]||"").replace(/\s/g,"").split(/;/g);
            for (var i=0;i<allowedTypes.length;i++) {
                if (cache[request.headers["host"] + "::" + uri + "::" + allowedTypes[i] + "::" + request.method]) {
                    type = allowedTypes[i];
                    break;
                }
            }
            return request.headers["host"] + "::" + uri + "::" + type + "::" + request.method;
        },
        controlRequestParse = function(request) {
            var controlDirectives = (request.headers["cache-control"]||"")
                    .replace(/\s/g,"").split(/;/g),
                control = {
                    "max-age" : null,
                    "no-cache" : false,
                    "no-store" : false,
                    "max-stale" : null,
                    "min-fresh" : null,
                    "no-transform" : false,
                    "only-if-cached" : false
                };
            var part;
            for (var i=0;i<controlDirectives.length;i++) {
                part = controlDirectives[i].split(/\=/);
                if (typeof control[part[0]] !== undefined) {
                    control[part[0]] = control[part[0]]===false?true:part[1]||null;
                }
            }
            return control;
        },
        controlResponseParse = function(responseHeaders) {
            var controlDirectives = (responseHeaders["cache-control"]||"")
                    .replace(/\s/g,"").split(/;/g),
                control = {
                    "public" : true,
                    "private" : false,
                    "max-age" : null,
                    "no-cache" : false,
                    "no-store" : false,
                    "max-age" : null,
                    "s-maxage" : null,
                    "no-transform" : false,
                    "must-revalidate" : false,
                    "proxy-revalidate" : false
                };
            var part;
            for (var i=0;i<controlDirectives.length;i++) {
                part = controlDirectives[i].split(/\=/);
                if (typeof control[part[0]] !== undefined) {
                    control[part[0]] = 
                        (control[part[0]]===false||control[part[0]]===true)?
                            true:part[1]||null;
                }
            }
            return control;
        };
        
    itself.cacheMaintenance = function(count) {
        console.log("cacheMaintenance starting: " + count);
        var age = 0, n = 0, stalest = null, staleRating = 0, newsr;
        count = count||1;
        for (var id in cache) {if (!Object[id]) {
            age = (new Date().valueOf()/1000)-(cache[id]["version-cache-time"]||0);
            if (cache[id].expired===true
                ||((cache[id]["max-age"]||0)<age&&cache[id]["max-age"]!==null)
            ) {
                    cache[id] = null;
                    delete cache[id];
                    cacheLength--;
                    if (++n>=count) {
                        break;
                    }
            } else {
                newsr = age/cache[id]["version-hits"];
                if (newsr>staleRating) {
                    staleRating = newsr;
                    stalest = id;
                }
            }
        }}
        if (count>n&&cacheLength>maxNumObjs*0.9) {
            cache[stalest] = null;
            delete cache[stalest];
            cacheLength--;
            n++;
        }
        console.log("purged " + n + " items from cache. cache length: " + cacheLength)
    };
    
    itself.loadCache = function(request, response) {
        if (request.method!=="GET"
            &&request.method!=="HEAD") {
            return; //cann't cache
        }
        var uri = request.url,
            statusCode,
            headers = { },
            chunks = [ ],
            size = 0,
            allowCache = true,
            beforeWriteHead = function(s, h) {
                statusCode = s;
                headers = h;
                return [s, h];
            },
            beforeWrite = function(chunk, encoding) {
                /*var data;
                if (chunk && typeof chunk.copy === "function") {
                    data = new Buffer(chunk.length);
                    chunk.copy(data, 0, 0, chunk.length);
                } else {
                    data = chunk;
                }*/
                size+=chunk?chunk.length:0; //not a perfect measure, but a start
                if (size>maxObjSize) {
                    allowCache = false;
                }
                chunks.push({"chunk":chunk, "encoding":encoding||"ascii"});
                return [chunk, encoding];
            },
            afterEnd = function() {
                if (!allowCache) {
                    return;
                }
                var reqControl = controlRequestParse(request),
                    resControl = controlResponseParse(headers);
                if (reqControl["no-cache"]===true) {
                    console.log("req no-cache");
                    return;
                }
                if (reqControl["no-store"]===true) {
                    console.log("req no-store");
                    return;
                }
                if (resControl["no-cache"]===true) {
                    console.log("res no-cache");
                    return;
                }
                if (resControl["private"]===true) {
                    console.log("private");
                    return;
                }
                var id = headers["host"] + "::" + uri + "::" + (headers["content-type"]||"*/*") + "::" + request.method,
                    rt = new Date().valueOf()/1000,
                    stale = cache[id]?true:false;
                cache[id] = {
                    "statusCode":statusCode, 
                    "chunks":chunks,
                    "headers":headers,
                    "version-cache-time":rt,
                    "first-request-time":cache[id]?cache[id]["first-request-time"]:rt,
                    "version-hits":0,
                    "hits":cache[id]?cache[id]["hits"]:0,
                    "version":(cache[id]?cache[id].version:0)+1,
                    "max-age":resControl["max-age"]||null,
                    "expired":false
                    };
                console.log("response cached: " + id);
                console.log("cache length: " + (stale?cacheLength:++cacheLength));   
            };
        
        aop.joinBefore(response, "end", beforeWrite);
        aop.joinBefore(response, "write", beforeWrite);
        aop.joinBefore(response, "writeHead", beforeWriteHead);
        aop.joinAfter(response, "end", afterEnd);
    };
    
    itself.checkCache = function(request, response, notFound) {
        var id = findCacheId(request);
        console.log("checking cache: " + id);
        var cachedResponse = cache[id];
        var reqControl = controlRequestParse(request);
        
        var miss = function() {
            if (cachedResponse) {
                cachedResponse.expired = true;
            }
            if (reqControl["only-if-cached"]===true) {
                response.beginHeader(404, {"content-type":"text/html"});
                response.end("<h1>Not Found</h1>");
            }
            console.log("cache miss");
            itself.loadCache(request, response);
            notFound(request, response);
            itself.cacheMaintenance(1);
        };
        
        if (cachedResponse) {
            console.log("cached match found");
            var age = (new Date().valueOf()/1000)-cachedResponse["version-cache-time"];
            console.log("age: " + age);
            if (cachedResponse["max-age"]||0<age&&cachedResponse["max-age"]!==null) {
                console.log("server max-age: " + cachedResponse["max-age"]);
                cacheNearMisses++;
                miss();
                return;
            }
            if (reqControl["max-age"]||0<age&&reqControl["max-age"]!==null) {
                console.log("max-age: " + reqControl["max-age"]);
                cacheNearMisses++;
                miss();
                return;
            }
            if (reqControl["max-stale"]||0<age&&reqControl["max-stale"]!==null) {
                console.log("max-stale: " + reqControl["max-stale"]);
                cacheNearMisses++;
                miss();
                return;
            }
            if (reqControl["min-fresh"]||0>age&&reqControl["min-fresh"]!==null) {
                console.log("min-fresh: " + reqControl["min-fresh"]);
                cacheNearMisses++;
                miss();
                return;
            }
            console.log("cache hit!");
            cachedResponse.hits++;
            cacheHits++;
            //console.log(JSON.stringify(cachedResponse, ["statusCode","headers"], 4));
            response.writeHead(cachedResponse.statusCode, cachedResponse.headers);
            for (var i=0; i<cachedResponse.chunks.length; i++) { if (cachedResponse.chunks[i].chunk) {
                try {
                    response.write(cachedResponse.chunks[i].chunk, i===0?cachedResponse.chunks[i].encoding:undefined);
                } catch (ex) {
                    console.log(ex.toString());
                }
            }}
            response.end();
            return;
        }
        
        cacheMisses++;
        miss();
    };

    return itself;
};